//----------------------------------------------------------------------------
// Copyright (C) 2003  Rafael H. Bordini, Jomi F. Hubner, et al.
// 
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// To contact the authors:
// http://www.inf.ufrgs.br/~bordini
// http://www.das.ufsc.br/~jomi
//
//----------------------------------------------------------------------------

package jason.asSyntax;

import jason.JasonException;
import jason.asSyntax.Trigger.TEOperator;
import jason.asSyntax.Trigger.TEType;
import jason.asSyntax.parser.ParseException;
import jason.bb.BeliefBase;

import java.util.*;
import java.util.logging.*;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Represents a set of plans used by an atomic expression
 * 
 * @has - plans 0..* Plan
 */
public class DeltaPlanLibrary implements Iterable<Plan> {
	
	/** a MAP from TE to a list of relevant plans */
	private Map<PredicateIndicator, List<Plan>> relPlans = new HashMap<PredicateIndicator, List<Plan>>();
	
	/**
	 * All plans as defined in the AS code (maintains the order of the plans)
	 */
	private List<Plan> plans = new ArrayList<Plan>();
	
	/** list of plans that have var as TE */
	private List<Plan> varPlans = new ArrayList<Plan>();
	
	/** A map from labels to plans */
	private Map<String, Plan> planLabels = new HashMap<String, Plan>();
	
	private Map<String, Boolean> ignoredPlans = new HashMap<String, Boolean>();
	
	/** The normal plan library of the agent. */
	private PlanLibrary pl;
	
	private Logger logger = Logger.getLogger(DeltaPlanLibrary.class.getName());
	
	public DeltaPlanLibrary(PlanLibrary planLibrary) {
		this.pl = planLibrary;
	}
	
	/**
	 * Add a new plan in PL. The source normally is "self" or the agent that sent this plan. If the PL already has a
	 * plan equals to the parameter p, only a new source is added.
	 * 
	 * If <i>before</i> is true, the plan will be added in the begin of the PlanLibrary; otherwise, it is added in the
	 * end.
	 * 
	 * @returns the plan just added
	 */
	public Plan add(Plan p, Term source, boolean before) throws ParseException {
		List<Plan> planList = pl.getPlans();
		int i = planList.indexOf(p);
		if (i < 0) {
			// add label, if necessary
			if (p.getLabel() == null) p.setLabel(pl.getUniqueLabel());
			p.getLabel().addSource(source);
			add(p, before);
		} else {
			p = planList.get(i);
			if (isIgnore(p)) {
				p.getLabel().addSource(source);
			} else {
				p = (Plan)p.clone();
				p.getLabel().addSource(source);
				ignoredPlans.put(p.getLabel().getFunctor(), null);
				addSafe(p, before);
			}
		}
		return p;
	}
	
	public void add(Plan p) throws ParseException {
		add(p, false);
	}
	
	/**
	 * Adds a plan into the plan library, either before or after all other plans depending on the boolean parameter.
	 * 
	 * @param p
	 *            The plan to be added to the plan library
	 * @param before
	 *            Whether or not to place the new plan before others
	 * @throws ParseException
	 *             if a plan already exists with the specified label
	 */
	public void add(Plan p, boolean before) throws ParseException {
		// test p.label
		if (p.getLabel() != null) {
			String label = p.getLabel().getFunctor();
			Plan planInPL = pl.get(label);
			if (planInPL != null) {
				// test if the new plan is equal, in this case, just add a source
				if (p.equals(planInPL)) {
					if (isIgnore(label)) {
						planInPL.getLabel().addSource(p.getLabel().getSources().get(0));
					} else {
						planInPL = (Plan)planInPL.clone();
						planInPL.getLabel().addSource(p.getLabel().getSources().get(0));
						ignoredPlans.put(label, null);
						addSafe(planInPL, before);
					}
					return;
				} else {
					throw new ParseException("There already is a plan with label " + p.getLabel(), p.getToken());
				}
			}
		}
		
		ignoredPlans.put(p.getLabel().getFunctor(), before);
		addSafe(p, before);
	}
	
	public void addSafe(Plan p, boolean before) {
		// add label, if necessary
		if (p.getLabel() == null) p.setLabel(pl.getUniqueLabel());
		
		// add self source
		if (!p.getLabel().hasSource()) p.getLabel().addAnnot(BeliefBase.TSelf);
		
		p.setAsPlanTerm(false); // it is not a term anymore
		
		planLabels.put(p.getLabel().getFunctor(), p);
		Trigger pte = p.getTrigger();
		if (pte.getLiteral().isVar()) {
			if (before) varPlans.add(0, p);
			else varPlans.add(p);
			// add plan p in all entries
			for(List<Plan> lp : relPlans.values())
				if (!lp.isEmpty() && lp.get(0).getTrigger().sameType(pte)) // only add if same type
				if (before) lp.add(0, p);
				else lp.add(p);
		} else {
			List<Plan> codesList = relPlans.get(pte.getPredicateIndicator());
			if (codesList == null) {
				codesList = new ArrayList<Plan>();
				// copy plans from var plans
				for(Plan vp : varPlans)
					if (vp.getTrigger().sameType(pte)) codesList.add(vp);
				relPlans.put(pte.getPredicateIndicator(), codesList);
			}
			if (before) codesList.add(0, p);
			else codesList.add(p);
		}
		
		if (before) plans.add(0, p);
		else plans.add(p);
	}
	
	/** return a plan for a label */
	public Plan get(String label) {
		return planLabels.get(label);
	}
	
	public int size() {
		return plans.size();
	}
	
	public List<Plan> getPlans() {
		return plans;
	}
	
	public Map<String, Plan> getLabels() {
		return Collections.unmodifiableMap(planLabels);
	}
	
	public Iterator<Plan> iterator() {
		return plans.iterator();
	}
	
	/** remove all plans */
	public void clear() {
		planLabels.clear();
		plans.clear();
		varPlans.clear();
		relPlans.clear();
		ignoredPlans.clear();
	}
	
	/**
	 * Remove a plan represented by the label <i>pLabel</i>. In case the plan has many sources, only the plan's source
	 * is removed.
	 */
	public boolean remove(Atom pLabel, Term source) {
		// find the plan
		Plan p = pl.get(pLabel.getFunctor());
		
		if (p != null) {
			if (isIgnore(pLabel.getFunctor())) {
				boolean hasSource = p.getLabel().delSource(source);
				
				// if no source anymore, remove the plan
				if (hasSource && !p.getLabel().hasSource()) {
					remove(pLabel.getFunctor());
				}
			} else {
				p = (Plan)p.clone();
				boolean hasSource = p.getLabel().delSource(source);
				
				if (hasSource) {
					ignoredPlans.put(pLabel.getFunctor(), null);
					// if has source, keep the plan
					if (p.getLabel().hasSource()) {
						addSafe(p, false);
					}
				}
			}
			return true;
		}
		return false;
	}
	
	/** remove the plan with label <i>pLabel</i> */
	public Plan remove(String pLabel) {
		Plan p = pl.get(pLabel);
		if (p == null) return null;
		
		if (isIgnore(pLabel)) {
			planLabels.remove(pLabel);
			
			// remove it from plans' list
			plans.remove(p);
			
			if (p.getTrigger().getLiteral().isVar()) {
				varPlans.remove(p);
				// remove p from all entries
				for(List<Plan> lp : relPlans.values())
					lp.remove(p);
			} else {
				List<Plan> codesList = relPlans.get(p.getTrigger().getPredicateIndicator());
				codesList.remove(p);
				if (codesList.isEmpty()) {
					// no more plans for this TE
					relPlans.remove(p.getTrigger().getPredicateIndicator());
				}
			}
		} else {
			ignoredPlans.put(pLabel, null);
		}
		
		return p;
	}
	
	public List<Plan> getRelPlans(Trigger te) {
		return relPlans.get(te.getPredicateIndicator());
	}
	
	public List<Plan> getVarPlans() {
		return varPlans;
	}
	
	public List<Plan> getIdlePlans() {
		return relPlans.get(PlanLibrary.TE_IDLE.getPredicateIndicator());
	}
	
	public DeltaPlanLibrary clone() {
		DeltaPlanLibrary dpl = new DeltaPlanLibrary(pl);
		for(Plan p : plans) {
			try {
				dpl.add((Plan)p.clone(), false);
			} catch (ParseException e) {
				e.printStackTrace();
			}
		}
		dpl.ignoredPlans.putAll(ignoredPlans);
		return dpl;
	}
	
	public String toString() {
		return plans.toString();
	}
	
	/** get as XML */
	public Element getAsDOM(Document document) {
		Element eplans = (Element)document.createElement("delta-plans");
		String lastFunctor = null;
		for(Plan p : plans) {
			String currentFunctor = p.getTrigger().getLiteral().getFunctor();
			if (lastFunctor != null && !currentFunctor.equals(lastFunctor)) {
				eplans.appendChild((Element)document.createElement("new-set-of-plans"));
			}
			lastFunctor = currentFunctor;
			eplans.appendChild(p.getAsDOM(document));
		}
		return eplans;
	}
	
	public boolean isIgnore(String label) {
		return ignoredPlans.containsKey(label);
	}
	
	public boolean isIgnore(Plan p) {
		return ignoredPlans.containsKey(p.getLabel().getFunctor());
	}
	
	public void commit() {
		for(Map.Entry<String, Boolean> e : ignoredPlans.entrySet()) {
			Boolean before = e.getValue();
			if (before == null) {
				Plan p = planLabels.remove(e.getKey());
				if (p != null) {
					pl.replace(e.getKey(), p);
				} else {
					pl.remove(e.getKey());
				}
			} else {
				pl.remove(e.getKey());
			}
		}
		
		for(Map.Entry<String, Plan> e : planLabels.entrySet()) {
			try {
				Boolean before = ignoredPlans.get(e.getKey());
				if (before == null) {
					before = false;
					logger.log(Level.SEVERE, "Error in commit() : 'before' should have a value.");
				}
				pl.add(e.getValue(), before);
			} catch (ParseException e1) {
				logger.log(Level.SEVERE, "Error in commit() : the plan label should be unique", e1);
				e1.printStackTrace();
			}
		}
		
		clear();
	}
}
